Skip to content

Added support of 2-3 bit symmetric mode to LoRA-QAT - #4158

Open
andreyanufr wants to merge 35 commits into
openvinotoolkit:developfrom
andreyanufr:aanuf/2_3_bit_training
Open

andreyanufr wants to merge 35 commits into
openvinotoolkit:developfrom
andreyanufr:aanuf/2_3_bit_training

Conversation

@andreyanufr

@andreyanufr andreyanufr commented Jul 30, 2026 •

Copy link
Copy Markdown
Collaborator

Changes

  1. LoRA-QAT now support two new modes: CompressWeightsMode.INT3_SYM and CompressWeightsMode.INT2_SYM.

  2. New function repack_weights was added to repack 4-8 bit weights to lower bit representation if possible.

Reason for changes

Support of 2-3 bit in compression in QAT and pytorch backend.

Related tickets

CVS-180191

Tests

In progress.

Weight compression - success

@github-actions github-actions Bot added NNCF OpenVINO Pull requests that updates NNCF OpenVINO API Public API-impacting changes labels Jul 30, 2026
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Sep 21, 2026
@andreyanufr
andreyanufr marked this pull request as ready for review September 24, 2026 14:57
Copilot AI lite review requested due to automatic review settings September 24, 2026 14:57
@andreyanufr
andreyanufr requested a review from a team as a code owner September 24, 2026 14:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved critical and moderate issues remain in low-bit stripping, OpenVINO repacking, and the example workflow.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 High severity · 2 Medium severity · 2 Low severity

Open (6)
What changed in this PR

Adds INT2/INT3 symmetric LoRA-QAT support and OpenVINO weight repacking.

Changes:

  • Adds 2/3-bit quantization mappings and strip validation.
  • Implements and exports repack_weights.
  • Adds tests, models, and a distillation QAT example with documentation.
File Summary and review notes
tests/​openvino/​native/​test_model_repacking.py Adds repacking coverage; assertions should validate graph correctness and numerical equivalence (nit, 2 votes).
tests/​openvino/​native/​models.py Adds a repacking test model.
src/​nncf/​torch/​function_hook/​strip.py 2/3-bit DQ still uses 4-bit decompression and packing (critical, 3 votes).
src/​nncf/​quantization/​quantize_model.py Exposes the repacking API.
src/​nncf/​quantization/​algorithms/​weight_compression/​torch_backend.py Adds INT2/INT3 LoRA quantization mappings.
src/​nncf/​quantization/​algorithms/​weight_compression/​openvino_backend.py Shape heuristic skips valid signed compressed weights, including 1-D tensors (critical, 2 votes).
src/​nncf/​quantization/​algorithms/​weight_compression/​algorithm.py Coordinates repacking; direct print calls should use the project logger (nit, 4 votes).
src/​nncf/​quantization/​__init__.py Exports repack_weights.
src/​nncf/​openvino/​quantization/​quantize_model.py Provides the OpenVINO repacking implementation.
src/​nncf/​__init__.py Exports the public API.
examples/​llm_compression/​torch/​distillation_qat_with_lora_2_3_bit/​requirements.txt Defines example dependencies.
examples/​llm_compression/​torch/​distillation_qat_with_lora_2_3_bit/​README.md Documents the example workflow.
examples/​llm_compression/​torch/​distillation_qat_with_lora_2_3_bit/​main.py Omits gate_proj equalization (moderate, 2 votes), skips meta-device weights (moderate, 3 votes), drops partial accumulation gradients (moderate, 1 vote), and does not return its documented metric (nit, 1 vote).
.ci/​cspell_dict.txt Adds example vocabulary.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/nncf/torch/function_hook/strip.py Outdated
Comment on lines +495 to +497
if w.device.type != "meta":
return w.data
return None
Comment thread src/nncf/quantization/algorithms/weight_compression/algorithm.py Outdated
Comment on lines +34 to +38
repacked_model = nncf.repack_weights(model)

element_types = get_constant_element_types(repacked_model)
assert ov.Type.u3 in element_types, "Expected u3 constant after repacking"
assert ov.Type.u2 in element_types, "Expected u2 constant after repacking"
2) Added arg for deterministic cuda execution. This arg is optional because it can slow down the training.
### 1. Create and activate a virtual environment

```bash
cd examples/llm_compression/torch/distillation_qat_with_lora_2_3_bit

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cd examples/llm_compression/torch/distillation_qat_with_lora_2_3_bit

Please keep same instruction steps in examples.

Comment on lines +45 to +55
### 2. Install the example dependencies from this folder

```bash
python -m pip install -r requirements.txt
```

### 3. Install NNCF and other dependencies

```bash
python3 -m pip install ../../../../ -r requirements.txt
```

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
### 2. Install the example dependencies from this folder
```bash
python -m pip install -r requirements.txt
```
### 3. Install NNCF and other dependencies
```bash
python3 -m pip install ../../../../ -r requirements.txt
```
### 2. Install NNCF and other dependencies
```bash
python3 -m pip install ../../../../ -r requirements.txt

?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about local requirements like datasets ? We do not have them in ../../../../ -r requirements.txt

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

../../../../ - installing NNCF with pakage's requarements
-r requirements.txt - installing example's requarements

Comment on lines +62 to +63
cd examples/llm_compression/torch/distillation_qat_with_lora_2_3_bit
python main.py --pretrained meta-llama/Llama-3.2-1B-Instruct --bits 3 --dataset pile-10k --epochs 1 --output_dir output

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cd examples/llm_compression/torch/distillation_qat_with_lora_2_3_bit
python main.py --pretrained meta-llama/Llama-3.2-1B-Instruct --bits 3 --dataset pile-10k --epochs 1 --output_dir output
python main.py

For example, just to run main.py with default arguemts

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

- `--scale_epochs`: extra epochs where only quantizer scales are trained
- `--linear_lr_scheduler`: use a linear learning-rate decay schedule
- `--batch_size`: accumulation target
- `--microbatch_size`: per-step microbatch size

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- `--microbatch_size`: per-step microbatch size
- `--microbatch_size`: per-step microbatch size
- `--full_determinism`: to enable deterministic QAT

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

- OpenVINO export is always performed at the end of the run, after checkpoint restoration and stripping.
- `--resume` reuses the checkpoint if present; otherwise the script initializes from scratch.

For more background on absorbable LoRA and low-bit training-time compression, see the project documentation for QAT LoRA usage and NNCF compression flows.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is necessary,
or add links

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread src/nncf/torch/function_hook/strip.py Outdated
if hook_module._qspec.half_range or hook_module._qspec.narrow_range:
msg += "Unexpected parameters of quantizers on strip: half_range and narrow_range should be False.\n"
if hook_module.num_bits not in [4, 8]:
if hook_module.num_bits not in [2, 3, 4, 8]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks this function should work for any num_bits, and this condition can be removed?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines +122 to +130
supported_src_types = {ov.Type.u4: 4, ov.Type.i4: 4, ov.Type.u8: 8, ov.Type.i8: 8}
if ov_node.tensor_view.element_type not in supported_src_types:
return False, 0, 0
bits = supported_src_types[ov_node.tensor_view.element_type]
asym = ov_node.tensor_view.element_type in [ov.Type.u8, ov.Type.u4]

# only repack for sym types are supported for now: i3, i2
if asym:
return False, 0, 0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
supported_src_types = {ov.Type.u4: 4, ov.Type.i4: 4, ov.Type.u8: 8, ov.Type.i8: 8}
if ov_node.tensor_view.element_type not in supported_src_types:
return False, 0, 0
bits = supported_src_types[ov_node.tensor_view.element_type]
asym = ov_node.tensor_view.element_type in [ov.Type.u8, ov.Type.u4]
# only repack for sym types are supported for now: i3, i2
if asym:
return False, 0, 0
supported_src_types = {ov.Type.i4: 4, ov.Type.i8: 8}
if ov_node.tensor_view.element_type not in supported_src_types:
return False, 0, 0

?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines +125 to +126
bits = supported_src_types[ov_node.tensor_view.element_type]
asym = ov_node.tensor_view.element_type in [ov.Type.u8, ov.Type.u4]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
bits = supported_src_types[ov_node.tensor_view.element_type]
asym = ov_node.tensor_view.element_type in [ov.Type.u8, ov.Type.u4]
bits = ov_node.tensor_view.element_type.get_bitwidth()
asym = ov_node.tensor_view.element_type.is_signed()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


ov_node = self.name_to_node_mapping[node.node_name]

supported_src_types = {ov.Type.u4: 4, ov.Type.i4: 4, ov.Type.u8: 8, ov.Type.i8: 8}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like now suppoerted only i4 and i8

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines +444 to +450
if hasattr(weight_compression_parameters, "sequence"):
# avoid track
is_all_8bit = all(
wc_params.compression_config.num_bits == 8 for wc_params in weight_compression_parameters.sequence
)
else:
is_all_8bit = all(wc_params.compression_config.num_bits == 8 for wc_params in weight_compression_parameters)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if hasattr(weight_compression_parameters, "sequence"):
# avoid track
is_all_8bit = all(
wc_params.compression_config.num_bits == 8 for wc_params in weight_compression_parameters.sequence
)
else:
is_all_8bit = all(wc_params.compression_config.num_bits == 8 for wc_params in weight_compression_parameters)
is_all_8bit = all(
wc_params.compression_config.num_bits == 8
for wc_params in getattr(weight_compression_parameters, "sequence", weight_compression_parameters)
)

Looks like track uses alwasy here but ok

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@anzr299 anzr299 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor

@@ -0,0 +1,879 @@
# Copyright (c) 2026 Intel Corporation

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can rename the example to QAD with Lora since it seems more suitable

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines +731 to +733
transformers.enable_full_determinism(42)
else:
transformers.set_seed(42)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need the else statemnet? does transformers.set_seed(42) affect full_determinism()?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This parameter can slow down training, so I suggest keeping it optional.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +135 to +141
max_val = max(int(weight_tensor.max().item()), abs(int(weight_tensor.min().item())))
optimal_bits = int(max_val).bit_length()

supported_optimal_bits = {2: ov.Type.u2, 3: ov.Type.u3}

# If the optimal bits is equal to the current bits, no need to repack
if optimal_bits == bits or optimal_bits not in supported_optimal_bits:
Comment on lines +266 to +267
CompressWeightsMode.INT2_SYM: QuantizationScheme.SYMMETRIC_LORA,
CompressWeightsMode.INT3_SYM: QuantizationScheme.SYMMETRIC_LORA,
opt = torch.optim.AdamW(param_to_train, weight_decay=weight_decay)

# Run tuning with distillation loss and validation after each epoch.
grad_accumulation_steps = args.batch_size // args.microbatch_size

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines +152 to +159
converted_const = opset.convert(compressed_const, ov.Type.f16)

zero_point_const = opset.constant(offset, dtype=ov.Type.i8, name=f"{const_node_name}/zero_point")
zero_point_const = opset.convert(zero_point_const, ov.Type.f16)

converted_const = opset.subtract(
converted_const, zero_point_const, name=f"{const_node_name}/zero_point/subtract"
)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


This is a real training pipeline for low-bit LLM optimization, not a simple inference example.

## Prerequisites

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any minimal or recommended HW? If so, let's mention it.


### Model and compression

- `--pretrained`: HF model ID or local path.

@MaximProshin MaximProshin Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it works with any model. Can we add a note somewhere which models this example was tested with?

@@ -0,0 +1,123 @@
# Quantization Aware Distillation with absorbable LoRA for 2/3-bit LLM compression

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's try to explain all abbreviations when they're mentioned first time

@github-actions github-actions Bot removed the API Public API-impacting changes label Oct 8, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation NNCF OpenVINO Pull requests that updates NNCF OpenVINO

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants